No description has been provided for this image

M2.991 · Aprenentatge automàtic · PAC1

2024-1 · Màster universitari en Ciència de dades (Data science)

Estudis d'Informàtica, Multimèdia i Telecomunicació

 
PAC 1: Preparació de dades

L'objectiu principal d'aquesta primera PAC és que us familiaritzeu amb l'entorn de treball que utilitzareu en la resta de pràctiques de l'assignatura. Aquest entorn estarà format per un conjunt de dependències relatives a certs mòduls de Python que seran necessaris per poder executar la vostra PAC de manera correcta. Aquestes dependències les gestionarem gràcies a l'ajuda d'Anaconda. Una altra de les eines fonamentals del que serà el vostre nou entorn de treball serà Jupyter, que us permetrà treballar amb Notebooks (fitxers *.ipynb) com el present enunciat.

Un altre dels aspectes més importants que cobrirem en aquesta primera PAC, tal com indica el títol, és el de la preparació de les dades. En aquesta PAC aprendrem a carregar un conjunt de dades o dataset i ens ajudarem d'eines de visualització per comprendre millor com es distribueixen les dades amb l'objectiu d'entendre com podem treure'n profit. A més, ens acostumarem a treballar amb conjunts d'entrenament i de prova per confirmar si les conclusions que traiem sobre una part de les mostres es poden generalitzar i extrapolar a la resta.

En resum, en aquesta pràctica veurem com aplicar diferents tècniques per a la càrrega i preparació de dades seguint els passos llistats a continuació:

  1. Càrrega d'un conjunt de dades (1 punt)
  2. Anàlisi de les dades (2.5 punts)
    2.1. Anàlisi estadístic bàsic
    2.2. Anàlisi exploratori de les dades
  3. Preprocessament de les dades (1.5 punts)
  4. Reducció de la dimensionalitat (2.5 punts)
  5. Conjunts desbalancejats de dades (2.5 punts)
    5.1. Oversampling

Important: cada un dels exercicis pot suposar diversos minuts d'execució, per la qual cosa l'entrega s'ha de fer en format notebook i en format html, on es vegi el codi, els resultats i comentaris de cada exercici. Es pot exportar el notebook a html des del menú File $\to$ Download as $\to$ HTML.

Important: existeix un tipus de cel·la especial per a albergar text. Aquest tipus de cel·la us serà molt útil per respondre a les diferents preguntes teòriques plantejades al llarg de cada PAC. Per canviar el tipus de cel·la a aquest tipus, escolliu en el menú: Cell $\to$ Cell Type $\to$ Markdown.

Important: la solució plantejada no ha d'utilitzar mètodes, funcions o paràmetres declarats "deprecated" en futures versions.

Important: no oblideu posar el vostre nom i cognoms a la següent cel·la.

Nom i cognoms: Toni Vives Cabaleiro

Toni Vives Cabaleiro

Per la realització de la pràctica, necessitarem importar els següents mòduls:

In [1]:
import numpy as np
import pandas as pd
from sklearn import preprocessing
from sklearn.manifold import TSNE
from sklearn.decomposition import PCA
from sklearn.model_selection import train_test_split
from imblearn.over_sampling import RandomOverSampler, SMOTE, ADASYN
import seaborn as sns
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)

import matplotlib
import matplotlib.pyplot as plt

pd.set_option('display.max_columns', None)
seed = 100

%matplotlib inline

Càrrega del conjunt de dades (1 punt)¶

Al llarg de tota la PAC treballarem amb el conjunt de dades anomenat Bank Marketing, que és un dels datasets disponibles al Repositori d'Aprenentatge Automàtic de la Universitat de Califòrnia a Irvine.

A l'enllaç [https://archive.ics.uci.edu/dataset/222/bank+marketing] teniu disponible tant el conjunt de dades Bank Marketing esmentat com tota la informació rellevant necessària per comprendre millor amb quin tipus de dades treballarem. En resum, les dades d'aquest dataset estan relacionades amb campanyes de màrqueting directe (trucades telefòniques) d'una institució bancària portuguesa. L'objectiu que es busca en aquest conjunt de dades és predir si el client contractarà un dipòsit a termini o no (variable y).

En primer lloc, haureu de carregar al Notebook el conjunt de dades amb el qual treballarem durant la resta de la PAC. Per fer-ho, podeu descarregar-lo manualment des de l'enllaç referit prèviament, tot i que us aconsellem que instal·leu i utilitzeu el mòdul ucimlrepo tal com s'explica a la pàgina del dataset.

Exercici: carregueu el conjunt de dades "Bank Marketing" i mostreu:
  • El nombre i els noms dels atributs descriptius (variables que podrien ser utilitzades per predir la variable objectiu "y").
  • El nombre de files (mostres) del conjunt de dades.
  • Verifiqueu si hi ha "missing values", i si és així, en quines columnes.

Suggeriment: si utilitzeu ucimlrepo, exploreu els atributs metadata i variables de l'objecte obtingut.

Suggeriment: separeu el conjunt de dades original en les variables "X" (atributs descriptius) i "y" (variable objectiu), tot i que potser us sigui útil en algun moment tenir-les també en un únic DataFrame combinat.

Començarem analitzant la informació que ens mostra el propi dataset en el repositori d'Aprenentatge Automàtic de la Universitat de Califòrnia a Irvine:

Taula de variables

Nom de la variable Rol Tipus Demogràfic Descripció
edat Característica Enter Edat
feina Característica Categòric Ocupació Tipus de feina (categòric: 'administrador', 'obrer', 'empresari', 'empleat domèstic', 'gerent', 'jubilat', 'autònom', 'serveis', 'estudiant', 'tècnic', 'aturat', 'desconegut')
estat civil Característica Categòric Estat civil Estat civil (categòric: 'divorciat', 'casat', 'solter', 'desconegut'; nota: 'divorciat' significa divorciat o vidu)
educació Característica Categòric Nivell d'educació (categòric: 'bàsic.4a', 'bàsic.6a', 'bàsic.9a', 'batxillerat', 'analfabet', 'curs professional', 'títol universitari', 'desconegut')
per defecte Característica Binari Té crèdit en mora?
saldo Característica Enter Saldo mitjà anual
habitatge Característica Binari Té préstec hipotecari?
préstec Característica Binari Té préstec personal?
contacte Característica Categòric Tipus de comunicació de contacte (categòric: 'mòbil', 'telèfon')
dia_de_la_setmana Característica Data Últim dia de contacte de la setmana
mes Característica Data Últim mes de contacte de l'any (categòric: 'gen', 'feb', 'mar', ..., 'nov', 'des')
durada Característica Enter Durada de l'últim contacte, en segons (numèric). Nota: Aquest atribut afecta considerablement el resultat final. Si la durada és 0, el resultat és "no". Aquesta entrada s'ha d'incloure només amb finalitats de referència i s'ha de descartar si es vol un model predictiu realista.
campanya Característica Enter Nombre de contactes realitzats durant aquesta campanya i per aquest client (numèric, inclou l'últim contacte)
dies de pau Característica Enter Nombre de dies des que es va contactar per última vegada al client d'una campanya anterior (numèric; -1 significa que el client no va ser contactat prèviament)
anterior Característica Enter Nombre de contactes realitzats abans d'aquesta campanya i per aquest client.
resultat Característica Categòric Resultat de la campanya de màrqueting anterior (categòric: 'fracàs', 'inexistent', 'èxit')
y Objectiu Binari El client ha subscrit un dipòsit a termini?
In [2]:
from ucimlrepo import fetch_ucirepo 
  
# fetch dataset 
bank_marketing = fetch_ucirepo(id=222) 
  
# data (as pandas dataframes) 
X = bank_marketing.data.features 
y = bank_marketing.data.targets 

df = X
df["y"] = y
In [3]:
# mostrem la metadata
bank_marketing.metadata
Out[3]:
{'uci_id': 222,
 'name': 'Bank Marketing',
 'repository_url': 'https://archive.ics.uci.edu/dataset/222/bank+marketing',
 'data_url': 'https://archive.ics.uci.edu/static/public/222/data.csv',
 'abstract': 'The data is related with direct marketing campaigns (phone calls) of a Portuguese banking institution. The classification goal is to predict if the client will subscribe a term deposit (variable y).',
 'area': 'Business',
 'tasks': ['Classification'],
 'characteristics': ['Multivariate'],
 'num_instances': 45211,
 'num_features': 16,
 'feature_types': ['Categorical', 'Integer'],
 'demographics': ['Age', 'Occupation', 'Marital Status', 'Education Level'],
 'target_col': ['y'],
 'index_col': None,
 'has_missing_values': 'yes',
 'missing_values_symbol': 'NaN',
 'year_of_dataset_creation': 2014,
 'last_updated': 'Fri Aug 18 2023',
 'dataset_doi': '10.24432/C5K306',
 'creators': ['S. Moro', 'P. Rita', 'P. Cortez'],
 'intro_paper': {'ID': 277,
  'type': 'NATIVE',
  'title': 'A data-driven approach to predict the success of bank telemarketing',
  'authors': 'Sérgio Moro, P. Cortez, P. Rita',
  'venue': 'Decision Support Systems',
  'year': 2014,
  'journal': None,
  'DOI': '10.1016/j.dss.2014.03.001',
  'URL': 'https://www.semanticscholar.org/paper/cab86052882d126d43f72108c6cb41b295cc8a9e',
  'sha': None,
  'corpus': None,
  'arxiv': None,
  'mag': None,
  'acl': None,
  'pmid': None,
  'pmcid': None},
 'additional_info': {'summary': "The data is related with direct marketing campaigns of a Portuguese banking institution. The marketing campaigns were based on phone calls. Often, more than one contact to the same client was required, in order to access if the product (bank term deposit) would be ('yes') or not ('no') subscribed. \n\nThere are four datasets: \n1) bank-additional-full.csv with all examples (41188) and 20 inputs, ordered by date (from May 2008 to November 2010), very close to the data analyzed in [Moro et al., 2014]\n2) bank-additional.csv with 10% of the examples (4119), randomly selected from 1), and 20 inputs.\n3) bank-full.csv with all examples and 17 inputs, ordered by date (older version of this dataset with less inputs). \n4) bank.csv with 10% of the examples and 17 inputs, randomly selected from 3 (older version of this dataset with less inputs). \nThe smallest datasets are provided to test more computationally demanding machine learning algorithms (e.g., SVM). \n\nThe classification goal is to predict if the client will subscribe (yes/no) a term deposit (variable y).",
  'purpose': None,
  'funded_by': None,
  'instances_represent': None,
  'recommended_data_splits': None,
  'sensitive_data': None,
  'preprocessing_description': None,
  'variable_info': 'Input variables:\n   # bank client data:\n   1 - age (numeric)\n   2 - job : type of job (categorical: "admin.","unknown","unemployed","management","housemaid","entrepreneur","student",\n                                       "blue-collar","self-employed","retired","technician","services") \n   3 - marital : marital status (categorical: "married","divorced","single"; note: "divorced" means divorced or widowed)\n   4 - education (categorical: "unknown","secondary","primary","tertiary")\n   5 - default: has credit in default? (binary: "yes","no")\n   6 - balance: average yearly balance, in euros (numeric) \n   7 - housing: has housing loan? (binary: "yes","no")\n   8 - loan: has personal loan? (binary: "yes","no")\n   # related with the last contact of the current campaign:\n   9 - contact: contact communication type (categorical: "unknown","telephone","cellular") \n  10 - day: last contact day of the month (numeric)\n  11 - month: last contact month of year (categorical: "jan", "feb", "mar", ..., "nov", "dec")\n  12 - duration: last contact duration, in seconds (numeric)\n   # other attributes:\n  13 - campaign: number of contacts performed during this campaign and for this client (numeric, includes last contact)\n  14 - pdays: number of days that passed by after the client was last contacted from a previous campaign (numeric, -1 means client was not previously contacted)\n  15 - previous: number of contacts performed before this campaign and for this client (numeric)\n  16 - poutcome: outcome of the previous marketing campaign (categorical: "unknown","other","failure","success")\n\n  Output variable (desired target):\n  17 - y - has the client subscribed a term deposit? (binary: "yes","no")\n',
  'citation': None}}
In [4]:
df.head(10)
Out[4]:
age job marital education default balance housing loan contact day_of_week month duration campaign pdays previous poutcome y
0 58 management married tertiary no 2143 yes no NaN 5 may 261 1 -1 0 NaN no
1 44 technician single secondary no 29 yes no NaN 5 may 151 1 -1 0 NaN no
2 33 entrepreneur married secondary no 2 yes yes NaN 5 may 76 1 -1 0 NaN no
3 47 blue-collar married NaN no 1506 yes no NaN 5 may 92 1 -1 0 NaN no
4 33 NaN single NaN no 1 no no NaN 5 may 198 1 -1 0 NaN no
5 35 management married tertiary no 231 yes no NaN 5 may 139 1 -1 0 NaN no
6 28 management single tertiary no 447 yes yes NaN 5 may 217 1 -1 0 NaN no
7 42 entrepreneur divorced tertiary yes 2 yes no NaN 5 may 380 1 -1 0 NaN no
8 58 retired married primary no 121 yes no NaN 5 may 50 1 -1 0 NaN no
9 43 technician single secondary no 593 yes no NaN 5 may 55 1 -1 0 NaN no

Hem fet la importació de les dades de la manera que s'ha especificat, mitjançant la llibreria ucimlrepo. d'aquí hem obtingut X i y, no obstant això, els hem unit per tal de poder realitzar posteriorment el train i test corresponent separant com nosaltres vulguem el dataset.

A continuació mostrarem una descripció de les dades i de la estructura que tenen.

In [5]:
print("size = ", df.shape)
df.describe()
size =  (45211, 17)
Out[5]:
age balance day_of_week duration campaign pdays previous
count 45211.000000 45211.000000 45211.000000 45211.000000 45211.000000 45211.000000 45211.000000
mean 40.936210 1362.272058 15.806419 258.163080 2.763841 40.197828 0.580323
std 10.618762 3044.765829 8.322476 257.527812 3.098021 100.128746 2.303441
min 18.000000 -8019.000000 1.000000 0.000000 1.000000 -1.000000 0.000000
25% 33.000000 72.000000 8.000000 103.000000 1.000000 -1.000000 0.000000
50% 39.000000 448.000000 16.000000 180.000000 2.000000 -1.000000 0.000000
75% 48.000000 1428.000000 21.000000 319.000000 3.000000 -1.000000 0.000000
max 95.000000 102127.000000 31.000000 4918.000000 63.000000 871.000000 275.000000
In [6]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 45211 entries, 0 to 45210
Data columns (total 17 columns):
 #   Column       Non-Null Count  Dtype 
---  ------       --------------  ----- 
 0   age          45211 non-null  int64 
 1   job          44923 non-null  object
 2   marital      45211 non-null  object
 3   education    43354 non-null  object
 4   default      45211 non-null  object
 5   balance      45211 non-null  int64 
 6   housing      45211 non-null  object
 7   loan         45211 non-null  object
 8   contact      32191 non-null  object
 9   day_of_week  45211 non-null  int64 
 10  month        45211 non-null  object
 11  duration     45211 non-null  int64 
 12  campaign     45211 non-null  int64 
 13  pdays        45211 non-null  int64 
 14  previous     45211 non-null  int64 
 15  poutcome     8252 non-null   object
 16  y            45211 non-null  object
dtypes: int64(7), object(10)
memory usage: 5.9+ MB

amb aquesta descripció de les dades numèriques, podem veure que hi ha una gran diversitat entre els valors de les dades, fet que per poder-los comparar, possiblement caldrà aplicar una normalització de les dades.

Podem observar com a l'imprimir el shape, el dataFrame te 45211 files per 17 columnes, però al extreure la info() del dataFrame, veiem que no totes les columnes tenen la mateixa quantitat de dades, fet que fa pensar que hi haurà dades "null". Ho mostrem a continuació:

In [7]:
# confirmem que no hi ha dades faltants
df.isna().sum()
Out[7]:
age                0
job              288
marital            0
education       1857
default            0
balance            0
housing            0
loan               0
contact        13020
day_of_week        0
month              0
duration           0
campaign           0
pdays              0
previous           0
poutcome       36959
y                  0
dtype: int64
In [8]:
colNa = []
for column in df.columns:
    if df[column].isna().sum() > 0:
        colNa.append([column,int(df[column].isna().sum())])
print(" la llista de columnes que contenen valors nuls es = ", colNa)
 la llista de columnes que contenen valors nuls es =  [['job', 288], ['education', 1857], ['contact', 13020], ['poutcome', 36959]]
Pregunta: El conjunt de dades proposat correspon a un problema d'aprenentatge automàtic supervisat o no?; Si és el cas, de quin tipus d'aprenentatge supervisat estaríem parlant?

Tal com diu el repositori:

"Les dades estan relacionades amb campanyes de màrqueting directe (trucades telefòniques) d'una institució bancària portuguesa. L'objectiu de la classificació és predir si el client subscriurà un dipòsit a termini (variable y)."

Al tenir la variable y com a etiqueta, podem definir el problema com a un problema d'aprenentatge automàtic supervisat. i tal com diu l'enunciat del repositori, l'objectiu és classificar si el client subscriurà o no un dipòsit a termini, per tant es una classificació d'una variable binària.

Es tracta d'un problema d'aprenentatge automàtic supervisat de classificació.

Anàlisi de les dades (2.5 punts)¶

En aquest apartat visualitzarem cadascuna de les columnes o features del conjunt de dades per comprendre millor quina distribució tenen.

Anàlisi estadística bàsica¶

Exercici: realitzeu un anàlisi estadística bàsica:
  • Variables categòriques:
    • Calculeu la freqüència.
    • Feu un gràfic de barres per cada variable.
  • Variables numèriques:
    • Calculeu estadístics descriptius bàsics: mitjana, mediana, desviació estàndard, ...
    • Feu un histograma per a cada variable.
Suggeriment: podeu utilitzar la llibreria "pandas" i les seves funcions "describe" i "value_counts", així com les funcions "bar", "hist" i "hist2d" de matplotlib.

Variables categòriques¶

A continuació començarem mostrant els gràfics de les variables categòriques.

Com hem vist anteriorment, els tipus de dades que tenim son o bé del format "object", o del format "int64". Ens aprofitarem d'aquesta informació per tal de separar els arrays de categories i els de valors numèrics.

In [9]:
#seleccionem les variables categoriques del dataframe
categorical_columns = df.select_dtypes(include=['object']).columns

A continuació recorrerem tot l'array i per cada variable categòrica, imprimirem el recompte de valors (freqüència) per cada atribut.

In [10]:
for categoric_variable in categorical_columns:
    print(df[categoric_variable].value_counts())
    print("suma de valors = ",df[categoric_variable].value_counts().sum())
    print("\n")
job
blue-collar      9732
management       9458
technician       7597
admin.           5171
services         4154
retired          2264
self-employed    1579
entrepreneur     1487
unemployed       1303
housemaid        1240
student           938
Name: count, dtype: int64
suma de valors =  44923


marital
married     27214
single      12790
divorced     5207
Name: count, dtype: int64
suma de valors =  45211


education
secondary    23202
tertiary     13301
primary       6851
Name: count, dtype: int64
suma de valors =  43354


default
no     44396
yes      815
Name: count, dtype: int64
suma de valors =  45211


housing
yes    25130
no     20081
Name: count, dtype: int64
suma de valors =  45211


loan
no     37967
yes     7244
Name: count, dtype: int64
suma de valors =  45211


contact
cellular     29285
telephone     2906
Name: count, dtype: int64
suma de valors =  32191


month
may    13766
jul     6895
aug     6247
jun     5341
nov     3970
apr     2932
feb     2649
jan     1403
oct      738
sep      579
mar      477
dec      214
Name: count, dtype: int64
suma de valors =  45211


poutcome
failure    4901
other      1840
success    1511
Name: count, dtype: int64
suma de valors =  8252


y
no     39922
yes     5289
Name: count, dtype: int64
suma de valors =  45211


Seguidament, presentarem els gràfics de barres de cada variable categòrica.

In [11]:
#recorrem el array de categorical_columns i fem el gràfic de barres corresponent
for column in categorical_columns:
    plt.figure(figsize=(10, 5))
    sns.countplot(x=df[column])
    plt.title(f'Gràfic de barres de {column}')
    plt.xlabel(column)
    plt.ylabel('Frequència')
    plt.xticks(rotation=45)
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Gràcies als gràfics, podem representar i visualitzar les dades de forma més senzilla i entenedora.

Variables numèriques¶

Realitzarem el mateix procès que abans, però aquest cop amb variables numèriques de tipus "int64".

Primer calcularem les freqüències, i després mostrarem aquestes en histogrames.

In [12]:
#seleccionem les variables numèriques del dataframe
numeric_columns = df.select_dtypes(include=['int64']).columns
In [13]:
for numeric_variable in numeric_columns:
    print(df[numeric_variable].value_counts())
    print("suma de valors = ",df[numeric_variable].value_counts().sum())
    print("\n")
age
32    2085
31    1996
33    1972
34    1930
35    1894
      ... 
95       2
93       2
92       2
88       2
94       1
Name: count, Length: 77, dtype: int64
suma de valors =  45211


balance
0        3514
1         195
2         156
4         139
3         134
         ... 
14204       1
8205        1
9710        1
7038        1
4416        1
Name: count, Length: 7168, dtype: int64
suma de valors =  45211


day_of_week
20    2752
18    2308
21    2026
17    1939
6     1932
5     1910
14    1848
8     1842
28    1830
7     1817
19    1757
29    1745
15    1703
12    1603
13    1585
30    1566
9     1561
11    1479
4     1445
16    1415
2     1293
27    1121
3     1079
26    1035
23     939
22     905
25     840
31     643
10     524
24     447
1      322
Name: count, dtype: int64
suma de valors =  45211


duration
124     188
90      184
89      177
104     175
114     175
       ... 
1286      1
1380      1
1723      1
2184      1
1233      1
Name: count, Length: 1573, dtype: int64
suma de valors =  45211


campaign
1     17544
2     12505
3      5521
4      3522
5      1764
6      1291
7       735
8       540
9       327
10      266
11      201
12      155
13      133
14       93
15       84
16       79
17       69
18       51
19       44
20       43
21       35
22       23
23       22
25       22
24       20
29       16
28       16
26       13
31       12
27       10
32        9
30        8
33        6
34        5
36        4
35        4
38        3
43        3
41        2
50        2
37        2
55        1
51        1
63        1
46        1
58        1
39        1
44        1
Name: count, dtype: int64
suma de valors =  45211


pdays
-1      36954
 182      167
 92       147
 183      126
 91       126
        ...  
 749        1
 769        1
 587        1
 778        1
 854        1
Name: count, Length: 559, dtype: int64
suma de valors =  45211


previous
0      36954
1       2772
2       2106
3       1142
4        714
5        459
6        277
7        205
8        129
9         92
10        67
11        65
12        44
13        38
15        20
14        19
17        15
16        13
19        11
23         8
20         8
18         6
22         6
24         5
27         5
29         4
21         4
25         4
30         3
26         2
37         2
28         2
38         2
51         1
275        1
58         1
32         1
40         1
55         1
35         1
41         1
Name: count, dtype: int64
suma de valors =  45211


A continuació mostrem els valors estadístics més importants de cada variable numèrica.

In [14]:
df[numeric_columns].describe()
Out[14]:
age balance day_of_week duration campaign pdays previous
count 45211.000000 45211.000000 45211.000000 45211.000000 45211.000000 45211.000000 45211.000000
mean 40.936210 1362.272058 15.806419 258.163080 2.763841 40.197828 0.580323
std 10.618762 3044.765829 8.322476 257.527812 3.098021 100.128746 2.303441
min 18.000000 -8019.000000 1.000000 0.000000 1.000000 -1.000000 0.000000
25% 33.000000 72.000000 8.000000 103.000000 1.000000 -1.000000 0.000000
50% 39.000000 448.000000 16.000000 180.000000 2.000000 -1.000000 0.000000
75% 48.000000 1428.000000 21.000000 319.000000 3.000000 -1.000000 0.000000
max 95.000000 102127.000000 31.000000 4918.000000 63.000000 871.000000 275.000000
In [15]:
#recorrem el array de numeric_columns i fem el gràfic de barres corresponent
for column in numeric_columns:
    plt.figure(figsize=(10, 5))
    sns.histplot(x=df[column],kde=True,bins=50)
    plt.title(f'Gràfic de barres de {column}')
    plt.xlabel(column)
    plt.ylabel('Frequència')
    plt.xticks(rotation=45)
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
Anàlisi: comenteu els resultats.

Per començar, tenim la variable numèrica day_of_week que sota el meu punt de vista, s'hauria de tractar com a variable categòrica, doncs aquesta no hauria de tenir diferent valor segons el dia que és, és a dir, el diumenge dia 15, no ha de tenir diferència sobre el diumenge dia 22, i aquest últim no ha de ser més important ni representatiu que el primer pel fet de tenir un valor major.

Tractant sobre les freqüències, podem observar el següent de cada tipus de variable:

Categòriques:

  • Podem observar que en aquest grup de variables, tenim quatre que contenen valors nuls: Job, Education, Contact i Poutcome. D'aquestes variables, si bé es cert que tant amb job i education podriem intentar crear el valor unknown ( doncs no falten moltes dades), amb les altres dues s'hauria de decidir que fer ja que falta més del 25% de les dades.
  • Els aspectes que destacraia son que el mes en que es fan més trucades es el maig, que les persones de feina "blue-collar", "management" i "technician" suposen més de la meitat de les consultes afectuades.
  • En general la majoria de trucades son a persones casades, i amb estudis secundaris o terciaris.
  • Clarament, han trucat a gent que no tenia altres crèdits en mora.
  • Finalment, ens centrem en que la variable objectiu demostra que no s'ha aconseguit mitjançant la campany de marqueting que la gent es subscrigui al dipòsit a termini.

Numèriques:

  • Podem veure com la tendència de les edats de la gent a la que han trucat, principalment es tracta de gent joveentre els 30 i 40 anys.
  • Es truca a la gent durant la part central de cada mes.
  • La variable duration ens mostra com en general no s'ha aconseguit que les trucades durn massa més de 3 minuts.
  • La gran majoria de persones, era la primera vegada que les trucaven.
  • Durant la campana, podem veure que majoritàriament, s'han trucat a les persones una vegada; és a dir que si la gent no acceptava el dpòsit a termini, no insistien de nou.

Anàlisi exploratòria de les dades¶

En aquest subapartat explorarem gràficament la relació dels atributs descriptius amb la variable objectiu i analitzarem les diferents correlacions.

Exercici: utilitzant una llibreria gràfica, com ara matplotlib, per a cadascuna de les variables categòriques, superposeu en un mateix gràfic el diagrama de barres per a cada valor possible de la variable objectiu, diferenciant amb un color diferent segons el valor de "y", és a dir, quan sigui "no" o quan sigui "yes". Afegiu una llegenda per saber a quina classe correspon cada histograma.

La finalitat és observar com es distribueix cadascun dels atributs en funció de la classe que tenen, per poder identificar de manera visual i ràpida si alguns atributs ens permeten predir millor que altres el valor de la variable objectiu.


Suggeriment: podeu utilitzar el paràmetre "alpha" en els gràfics perquè es puguin apreciar els dos histogrames.
In [16]:
for column in categorical_columns[:-1]:
    fig, axes = plt.subplots(1, 2, figsize=(20, 5))
    
    # Obtenim l'ordre de l'eix de les "X"
    freq_order = df[column].value_counts().index
    
    # gràfic de freqüència
    sns.countplot(x=df[column], hue=df["y"], ax=axes[0], order=freq_order)
    axes[0].set_title(f'Gràfic de barres de freqüències de {column}')
    axes[0].set_xlabel(column)
    axes[0].set_ylabel('Frequència')
    axes[0].tick_params(axis='x', rotation=45)
    
    # Calculem els percentatges
    counts = df.groupby([column, 'y'], observed=True).size().unstack(fill_value=0)
    percentages = counts.apply(lambda x: 100 * x / x.sum(), axis=1)
    
    # Reorganitzem les dades
    percentages = percentages.stack().reset_index().rename(columns={0: 'percentatge'})
    
    # grafiquem les dades segons el seu percentatge
    sns.barplot(x=percentages[column], y=percentages['percentatge'], hue=percentages['y'], ax=axes[1], order=freq_order)
    axes[1].set_title(f'Gràfic de barres en percentatges de {column}')
    axes[1].set_xlabel(column)
    axes[1].set_ylabel('Percentatge(%)')
    axes[1].tick_params(axis='x', rotation=45)
    
  
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
Exercici: de la mateixa manera que en l'exercici anterior, superposeu els histogrames per als diferents valors de "y" per a cada variable numèrica.
In [17]:
df_numeric = df.copy()
for column in numeric_columns:
    fig, axes = plt.subplots(1, 2, figsize=(20, 5))
    
    # Dividim la variable en intervals (bins) utilitzant .loc per evitar l'error
    df_numeric.loc[:, f'{column}_bins'] = pd.cut(df_numeric[column], bins=30)
    
    # Mantenir "y" com a última columna
    columns = [col for col in df_numeric.columns if col != 'y'] + ['y']
    df_numeric = df_numeric[columns]
    
    # Obtenim l'ordre de l'eix X basat en les freqüències
    bin_order = df_numeric[f'{column}_bins'].value_counts().index
    
    # Gràfic de barres amb les freqüències per intervals
    sns.countplot(x=df_numeric[f'{column}_bins'], hue=df_numeric["y"], ax=axes[0], order=bin_order)
    axes[0].set_title(f'Gràfic de barres de freqüències (per intervals) de {column}')
    axes[0].set_xlabel(f'{column} (intervals)')
    axes[0].set_ylabel('Frequència')
    axes[0].tick_params(axis='x', rotation=45)
    
    counts = df_numeric.groupby([f'{column}_bins', 'y'], observed=True).size().unstack(fill_value=0)
    percentages = counts.apply(lambda x: 100 * x / x.sum(), axis=1)
    
    # Reorganitzem les dades
    percentages = percentages.stack().reset_index().rename(columns={0: 'percentatge'})
    
    # Gràfic de barres amb percentatges per intervals
    sns.barplot(x=percentages[f'{column}_bins'], y=percentages['percentatge'], hue=percentages['y'], ax=axes[1], order=bin_order)
    axes[1].set_title(f'Gràfic de barres en percentatges (per intervals) de {column}')
    axes[1].set_xlabel(f'{column} (intervals)')
    axes[1].set_ylabel('Percentatge (%)')
    axes[1].tick_params(axis='x', rotation=45)
    
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
In [18]:
df_numeric.describe()
Out[18]:
age balance day_of_week duration campaign pdays previous
count 45211.000000 45211.000000 45211.000000 45211.000000 45211.000000 45211.000000 45211.000000
mean 40.936210 1362.272058 15.806419 258.163080 2.763841 40.197828 0.580323
std 10.618762 3044.765829 8.322476 257.527812 3.098021 100.128746 2.303441
min 18.000000 -8019.000000 1.000000 0.000000 1.000000 -1.000000 0.000000
25% 33.000000 72.000000 8.000000 103.000000 1.000000 -1.000000 0.000000
50% 39.000000 448.000000 16.000000 180.000000 2.000000 -1.000000 0.000000
75% 48.000000 1428.000000 21.000000 319.000000 3.000000 -1.000000 0.000000
max 95.000000 102127.000000 31.000000 4918.000000 63.000000 871.000000 275.000000
Anàlisi:
Mirant els gràfics de barres i els histogrames, quins atributs semblen tenir més pes al moment de predir si es contractarà un dipòsit a termini o no? Creus que amb aquests atributs serà suficient per poder determinar si es contractarà o no el dipòsit?

Fent un analisi dels gràfics de barres, histogrames i les dades estadístiques, podem observar el següent:

Variables numèriques:

  • En la variable duration, es mostra una clara diferència en la distribució entre els clients que contracten el dipòsit i els que no. Les trucades més llargues semblen estar associades amb una major probabilitat de contractació. És un indicador important perquè pot reflectir un major interès o disponibilitat per part del client.
  • Amb la variable campaign podem veure com de persistent ha estat el banc amb un mateix client, això pot indicar que si son mes persistents, hi ha més opcions de convencer al client.No obstant això, l'impacte és menor en comparació amb duration.
  • La variable pdays pot ser rellevant per determinar l'efectivitat de la campanya i el moment òptim per recontactar.
  • Tot i que la major part dels clients es concentren en balanços (balance) baixos, hi ha una petita proporció de clients amb un balanç més alt que també mostren un major percentatge de contractació del dipòsit. Això podria ser rellevant, ja que els clients amb més recursos poden tenir més probabilitats de contractar productes financers.

Variables categòriques:

  • A la variable job es pot observar que certes ocupacions com management, retired i student tenen una proporció més alta de clients que contracten el dipòsit en comparació amb ocupacions com blue-collar. Això suggereix que el tipus de feina pot ser un factor rellevant, ja que indica el perfil socioeconòmic del client.
  • Els clients amb educació (education) terciària tenen una proporció més alta de contractació del dipòsit, mentre que els amb educació secundària o primària tenen una taxa de conversió menor. El nivell educatiu pot ser un bon predictor, ja que està relacionat amb la capacitat d'estalvi i la disposició a contractar productes financers.
  • Certs mesos (month), com may, mostren un volum molt alt de trucades però una baixa taxa de conversió. D'altra banda, mesos com oct o dec tenen menys trucades però una proporció relativament més alta de clients que contracten. El mes en què es fa la trucada pot influir en la probabilitat d’èxit, possiblement per factors estacionals o de campanya.
  • Els clients amb un success en una campanya anterior (poutcome) tenen una taxa de conversió molt més alta que altres categories. Això indica que l'historial d'interacció amb el client pot ser un factor molt determinant.
  • A la variable contact observem com les trucades a través del cellular semblen tenir una major taxa d’èxit en comparació amb les trucades telefòniques tradicionals (telephone). Aquest atribut podria ser important per identificar quin canal de contacte és més efectiu.

Si bé es cert que amb les dades que tenim, podem cobrir areas com Demografia, economia, context (mesos)... i això ens permetria construir un model predictiu força eficaç, podrien faltar variables com els salaris mensuals, el marge d'estalvi entre d'altres. Per altre banda, si bé es cert que tenim moltes dades, aquestes estan esbiaixades doncs hi ha molt més percentatge de dades on la variable objectiu es "no", que no pas "sí", i això podria fer que qualsevol model predictiu aprenguès malament i per conseqüent, no predigui correctament si es contractarà o no el dipòsit. Així doncs, sí que és suficient per començar, a fer coses però s'ha d'arreglar el problema de l 'esbiaix de les dades'.

Exercici: calculeu i mostreu la correlació entre les variables numèriques.

en el moment de correlacionar dades, em sembla molt interessant mostrar primer tots els gràfics enfrentats i després fer la matriu de correlació.

In [19]:
g = sns.pairplot(data=df,
                 corner=True,
                 hue="y")
g.fig.suptitle("Relació entre variables numèriques del dataset",
               va="baseline",
               ha="center",
               fontsize=16)
Out[19]:
Text(0.5, 0.98, 'Relació entre variables numèriques del dataset')
No description has been provided for this image
In [20]:
# Calculem la matriu de correlació
df['y_binary'] = df['y'].map({'yes': 1, 'no': 0})
numeric_columns_with_target = df.select_dtypes(include=['int64']).columns
correlation_matrix = df[numeric_columns_with_target].corr()

# Visualitzem la matriu de correlació amb un heatmap
plt.figure(figsize=(10, 8))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', linewidths=0.5)
plt.title('Correlation Matrix of Numerical Variables')
plt.show()
No description has been provided for this image
Anàlisi: comenta els resultats.

Podem veure com duration és clau doncs te una correlació de 0.39, això significa que com més llarga és la durada de la trucada, més probable és que el client acabi contractant el dipòsit. Aquesta és una observació lògica, ja que els clients que mostren interès tendeixen a mantenir trucades més llargues.

La variable pdays (dies des de l'últim contacte) també mostra una lleugera correlació positiva la variable objectiu. Això indica que el temps transcorregut des de l'últim contacte pot influir en la probabilitat que el client contracti el dipòsit, tot i que aquesta influència és moderada.

No obstant això, podem veure que les correlacions son en general molt baixes. Caldria observar si modificant el dataset (eliminant el esbiaix de dades) podriem treure alguna cosa més en clar.

Preprocessament de les dades (1.5 punts)¶

Un cop analitzats els atributs descriptius, és el moment de preparar-los perquè ens siguin útils amb vista a predir valors.

A partir d’aquest punt, per simplicitat, treballarem únicament amb els atributs numèrics.

En aquest apartat:

  • Estandarditzarem (o normalitzarem) els valors dels atributs descriptius numèrics perquè les seves escales no siguin molt diferents.
  • Separarem el conjunt de dades original en dos subconjunts: entrenament i test.
  • Pregunta: quina tècnica podríem fer servir per codificar els atributs categòrics de format numèric?

    Hi ha moltes tècniques que es poden utilitzar per codificar variables categòriques, com ara:

    • Label Encoding
    • One-Hot Encoding
    • Target Encoding
    • Mean Encoding
    • Frequency Encoding

    Cada tècnica és més eficient segons el tipus de tasca. En el nostre cas, la millor opció seria utilitzar One-Hot Encoding, ja que els valors representen categories sense cap ordre implícit.

    Quan diem que les categories no tenen un ordre, ens referim a casos com el de la columna marital amb els valors "married", "single" i "divorced". Si utilitzéssim Label Encoding per codificar aquestes categories, es podrien assignar valors com 1, 2 i 3. Això faria que "divorced" tingués un valor més alt que "married" o "single", cosa que podria induir el model a pensar que hi ha una jerarquia entre aquestes categories, creant biaixos en les prediccions.

    Amb One-Hot Encoding, evitem aquest problema perquè creem noves columnes per a cada categoria (en aquest cas, "married", "single" i "divorced") i assignem valors binaris (0 o 1) segons la presència o absència d'aquesta categoria en cada registre. D'aquesta manera, no hi ha cap ordre o relació numèrica implícita entre les categories, eliminant possibles biaixos.

    Exercici: conserva els atributs descriptius numèrics i estandarditza’ls, aquest serà el nou conjunt d’atributs descriptius amb el qual treballarem a partir d’ara.
    Suggeriment: utilitzeu "StandardScaler" de preprocessing.
    In [21]:
    from sklearn.preprocessing import StandardScaler
    
    numdf = df[numeric_columns]
    scaler = StandardScaler()
    norm_data = scaler.fit_transform(numdf)
    normdf = pd.DataFrame(norm_data,columns=numdf.columns)
    normdf.head(10)
    
    Out[21]:
    age balance day_of_week duration campaign pdays previous
    0 1.606965 0.256419 -1.298476 0.011016 -0.569351 -0.411453 -0.25194
    1 0.288529 -0.437895 -1.298476 -0.416127 -0.569351 -0.411453 -0.25194
    2 -0.747384 -0.446762 -1.298476 -0.707361 -0.569351 -0.411453 -0.25194
    3 0.571051 0.047205 -1.298476 -0.645231 -0.569351 -0.411453 -0.25194
    4 -0.747384 -0.447091 -1.298476 -0.233620 -0.569351 -0.411453 -0.25194
    5 -0.559037 -0.371551 -1.298476 -0.462724 -0.569351 -0.411453 -0.25194
    6 -1.218254 -0.300608 -1.298476 -0.159841 -0.569351 -0.411453 -0.25194
    7 0.100181 -0.446762 -1.298476 0.473107 -0.569351 -0.411453 -0.25194
    8 1.606965 -0.407679 -1.298476 -0.808322 -0.569351 -0.411453 -0.25194
    9 0.194355 -0.252657 -1.298476 -0.788906 -0.569351 -0.411453 -0.25194
    In [22]:
    normdf.describe()
    
    Out[22]:
    age balance day_of_week duration campaign pdays previous
    count 4.521100e+04 4.521100e+04 4.521100e+04 4.521100e+04 4.521100e+04 4.521100e+04 4.521100e+04
    mean 2.112250e-16 1.760208e-17 1.257292e-17 6.035001e-17 3.017500e-17 2.011667e-17 4.023334e-17
    std 1.000011e+00 1.000011e+00 1.000011e+00 1.000011e+00 1.000011e+00 1.000011e+00 1.000011e+00
    min -2.159994e+00 -3.081149e+00 -1.779108e+00 -1.002478e+00 -5.693506e-01 -4.114531e-01 -2.519404e-01
    25% -7.473845e-01 -4.237719e-01 -9.380027e-01 -6.025167e-01 -5.693506e-01 -4.114531e-01 -2.519404e-01
    50% -1.823406e-01 -3.002800e-01 2.326031e-02 -3.035165e-01 -2.465603e-01 -4.114531e-01 -2.519404e-01
    75% 6.652252e-01 2.158743e-02 6.240497e-01 2.362370e-01 7.622994e-02 -4.114531e-01 -2.519404e-01
    max 5.091402e+00 3.309478e+01 1.825628e+00 1.809470e+01 1.944365e+01 8.297431e+00 1.191360e+02
    Exercici: separa els atributs descriptius escalats i la variable objectiu en els subconjunts d’entrenament i de prova.
    Suggeriment: per separar entre entrenament i prova podeu utilitzar "train_test_split" de sklearn.

    El primer que farem serà unir el nostre dataframe "normdf" amb la variable independent "y"

    In [23]:
    normdf["y"] = df["y"]
    normdf.head(10)
    
    Out[23]:
    age balance day_of_week duration campaign pdays previous y
    0 1.606965 0.256419 -1.298476 0.011016 -0.569351 -0.411453 -0.25194 no
    1 0.288529 -0.437895 -1.298476 -0.416127 -0.569351 -0.411453 -0.25194 no
    2 -0.747384 -0.446762 -1.298476 -0.707361 -0.569351 -0.411453 -0.25194 no
    3 0.571051 0.047205 -1.298476 -0.645231 -0.569351 -0.411453 -0.25194 no
    4 -0.747384 -0.447091 -1.298476 -0.233620 -0.569351 -0.411453 -0.25194 no
    5 -0.559037 -0.371551 -1.298476 -0.462724 -0.569351 -0.411453 -0.25194 no
    6 -1.218254 -0.300608 -1.298476 -0.159841 -0.569351 -0.411453 -0.25194 no
    7 0.100181 -0.446762 -1.298476 0.473107 -0.569351 -0.411453 -0.25194 no
    8 1.606965 -0.407679 -1.298476 -0.808322 -0.569351 -0.411453 -0.25194 no
    9 0.194355 -0.252657 -1.298476 -0.788906 -0.569351 -0.411453 -0.25194 no

    A continuació dividirem en train i en test el nostre dataframe.

    In [24]:
    from sklearn.model_selection import train_test_split
    X_train,X_test,y_train,y_test = train_test_split(normdf.iloc[:,:-1],normdf["y"], test_size=0.3, random_state=42)
    print("X train: ")
    print(X_train.head())
    print("\n X test")
    print(X_test.head())
    print("\nY train: ")
    print(y_train.head())
    print("\n Y test")
    print(y_test.head())
    
    X train: 
                age   balance  day_of_week  duration  campaign     pdays  previous
    10747 -0.464863 -0.447419     0.143418 -0.408361  0.399020 -0.411453 -0.251940
    26054  1.418617 -0.383046     0.383734  0.209055  0.076230 -0.411453 -0.251940
    9125   0.476877 -0.447419    -1.298476 -0.680179 -0.246560 -0.411453 -0.251940
    41659  0.006007  0.677803    -1.779108  0.170224 -0.569351  0.787017  1.918749
    4443  -0.276515 -0.447419     0.503892 -0.652997 -0.569351 -0.411453 -0.251940
    
     X test
                age   balance  day_of_week  duration  campaign     pdays  previous
    3776  -0.088167 -0.256926     0.023260 -0.256919 -0.569351 -0.411453 -0.251940
    9928   0.571051  0.749402    -0.817845 -0.680179 -0.246560 -0.411453 -0.251940
    33409 -1.500776 -0.270721     0.503892 -0.124893 -0.569351 -0.411453 -0.251940
    31885  0.100181  0.134898    -0.817845  0.205172 -0.569351  2.954251  0.182198
    15738  1.418617 -0.376149     0.624050 -0.532621 -0.246560 -0.411453 -0.251940
    
    Y train: 
    10747    no
    26054    no
    9125     no
    41659    no
    4443     no
    Name: y, dtype: object
    
     Y test
    3776     no
    9928     no
    33409    no
    31885    no
    15738    no
    Name: y, dtype: object
    
    Anàlisi: valora si la decisió de transformar el conjunt de dades (estandardització) abans de realitzar la separació del conjunt de dades en els subconjunts d’entrenament i prova, és una bona idea.

    No és una bona idea estandarditzar les dades abans de separar-les en subconjunts d’entrenament i prova. Si l'estandarització es fa abans, el procés utilitza informació de tot el conjunt de dades per calcular les mitjanes i desviacions estàndard, incloent el subconjunt de prova, cosa que pot introduir biaixos. La manera correcta és separar primer les dades i després estandarditzar només el conjunt d’entrenament, aplicant els mateixos paràmetres al conjunt de prova per assegurar una avaluació justa.

    Anàlisi: en aquest exercici hem estandarditzat els valors dels atributs descriptius perquè les seves escales no siguin molt diferents (unificar-les). Què ens aporta estandarditzar els atributs descriptius?, hi ha alguna situació o escenari on sigui imprescindible?

    Quan les variables tenen escales molt diferents, un model de machine learning pot donar més importància a les variables amb valors més grans. Per exemple, si tens una variable amb valors entre 1 i 10 (com l'edat) i una altra amb valors entre 0.01 i 10000 (com els ingressos), el model pot prestar més atenció a la segona variable simplement perquè els seus valors són més grans. L'estandardització assegura que totes les variables tinguin una mitjana de 0 i una desviació estàndard de 1, permetent que totes contribueixin de manera equilibrada al model.

    Per altre banda, molts algoritmes de machine learning com la regressió logística, les xarxes neuronals o els mètodes de gradient descendent necessiten dades estandarditzades per a convergir de manera més eficient. L'estandardització ajuda a que l'entrenament sigui més ràpid i estable, ja que evita que algunes variables amb valors grans afectin el procés d'optimització.

    Reducció de la dimensionalitat (2.5 punts)¶

    En aquest apartat reprendrem l'anàlisi gràfica de la distribució de la classe al llarg de les mostres del conjunt de dades. En el segon apartat vam poder observar si les variables descriptives per separat eren molt prometedores o no per predir la classe. Aquí intentarem determinar si la seva combinació pot ajudar-nos a establir si es contractarà el dipòsit de manera més eficaç que utilitzant els atributs per separat. Amb aquest propòsit, reduirem la dimensionalitat del problema a només dos atributs, que seran la projecció dels atributs descriptius originals, i observarem com es distribueixen les mostres de cada classe.

    Exercici:
    • Aplica el mètode de reducció de la dimensionalitat Principal Component Analysis (PCA) per reduir a 2 dimensions el conjunt de dades complet amb tots els atributs (*features*).
    • Genera un gràfic en 2D amb el resultat del PCA utilitzant colors diferents per a cadascuna de les classes de la resposta, amb l'objectiu de visualitzar si és possible separar eficientment les classes amb aquest mètode.

    NOTA: Tingueu cura de no incloure la variable objectiu en la reducció de dimensionalitat. Volem explicar la variable objectiu en funció de la resta de variables reduïdes a dues dimensions.


    Suggeriment: no és necessari que programeu l'algorisme de PCA, podeu utilitzar la implementació disponible a la llibreria de scikit-learn.
    In [25]:
    from sklearn.decomposition import PCA
    
    pca = PCA(n_components=2)
    X_pca = pca.fit_transform(normdf.iloc[:,:-1])
    
    pca_df = pd.DataFrame(X_pca, columns=['PC1', 'PC2'])
    
    # Afegir la columna de classe (df['y']) per a utilitzar-la com a color
    pca_df['Target'] = df['y']
    
    # Generar el gràfic 2D amb colors diferents per a les classes
    plt.figure(figsize=(10, 8))
    sns.scatterplot(data=pca_df, x='PC1', y='PC2', hue='Target', palette='coolwarm', alpha=0.7)
    plt.title('PCA (2 Components) amb el Target pintat (yes/no)')
    plt.xlabel('Component Principal 1')
    plt.ylabel('Component Principal 2')
    plt.grid(True)
    plt.show()
    
    No description has been provided for this image
    Exercici:
    • Repeteix la reducció de la dimensionalitat, però en aquest cas utilitzant TSNE. Podeu trobar més informació sobre aquest algorisme en l'enllaç: https://distill.pub/2016/misread-tsne/
    • Igual que abans, genereu un gràfic en 2D amb el resultat del TSNE fent servir colors diferents per a cadascuna de les classes de la resposta (y), amb l'objectiu de visualitzar si és possible separar eficientment les classes amb aquest mètode.

    Suggeriment: no és necessari que programeu l'algorisme TSNE, podeu fer servir la implementació disponible a la llibreria de scikit-learn.
    Suggeriment: a part d'especificar el nombre de components, proveu els paràmetres "learning_rate" i "perplexity".
    In [26]:
    from sklearn.manifold import TSNE
    
    
    # fem la variable y binaria
    df['y_binary'] = df['y'].apply(lambda x: 1 if x == 'yes' else 0)
    
    # reduim a 2 components amb t-SNE
    tsne = TSNE(n_components=2, learning_rate=100, perplexity=30, random_state=42, early_exaggeration=12)
    X_tsne = tsne.fit_transform(normdf.iloc[:,:-1])
    
    # Convertim a dataframe
    tsne_df = pd.DataFrame(X_tsne, columns=['Component 1', 'Component 2'])
    tsne_df['Target'] = df['y_binary']
    
    # Plot
    plt.figure(figsize=(10, 8))
    sns.scatterplot(data=tsne_df, x='Component 1', y='Component 2', hue='Target', palette='coolwarm', alpha=0.7)
    plt.title('Resultat de t-SNE  (2 Components) amb el target pintat')
    plt.xlabel('Component 1')
    plt.ylabel('Component 2')
    plt.grid(True)
    plt.show()
    
    C:\Users\tvive\anaconda3\envs\uoc20241pec1\Lib\site-packages\joblib\externals\loky\backend\context.py:136: UserWarning: Could not find the number of physical cores for the following reason:
    found 0 physical cores < 1
    Returning the number of logical cores instead. You can silence this warning by setting LOKY_MAX_CPU_COUNT to the number of cores you want to use.
      warnings.warn(
      File "C:\Users\tvive\anaconda3\envs\uoc20241pec1\Lib\site-packages\joblib\externals\loky\backend\context.py", line 282, in _count_physical_cores
        raise ValueError(f"found {cpu_count_physical} physical cores < 1")
    
    No description has been provided for this image
    Anàlisi: observant els dos gràfics, consideres que la reducció de dimensionalitat ha funcionat bé?. Ha aconseguit separar les classes correctament?. Quin dels dos mètodes ha funcionat millor?. Per què obtenim resultats tan diferents?

    En el cas de la reducció de dimensionalitat amb PCA, podem observar que les dues classes (yes i no) es troben força superposades, la qual cosa indica que no hi ha una separació clara entre les dues. Per tant podem dir que PCA no ha funcionat de manera efectiva. D'altra banda, el gràfic obtingut amb t-SNE mostra una separació lleugerament millor. Tot i que les classes segueixen una mica barrejades, t-SNE ha aconseguit captar certes àrees on els punts es separen més clarament([(0,40),(-20,20)]), de manera que aquest mètode ens permetria separar millor en base a la variable objectiu.

    Els resultats són tan diferents perquè PCA i t-SNE aborden la reducció de dimensionalitat de maneres molt diferents. PCA se centra en maximitzar la variància global de les dades, i això funciona bé quan les dimensions de major variància coincideixen amb les que separen les classes. No obstant això, si les diferències entre les classes no es troben en les dimensions de major variància, com sembla ser el cas aquí, PCA no serveix. D'altra banda, t-SNE prioritza la preservació de les distàncies locals entre punts, la qual cosa li permet capturar patrons subtils i diferenciacions entre classes que no depenen de la variància global. Per això, en aquest cas, t-SNE ha funcionat millor en la separació de les classes.

    Pregunta: Què en penses de TSNE com a opció per reduir la dimensionalitat? Què et sembla que només tingui el mètode "fit_transform" però no tingui "transform"? Coneixes alguna altra opció que, amb prestacions similars, eviti els problemes que té TSNE?

    t-SNE és una opció molt potent per a la reducció de dimensionalitat quan l'objectiu és visualitzar dades complexes en espais de baixa dimensió, especialment quan es busca captar agrupacions locals. És particularment útil per a tasques de visualització, ja que pot capturar estructures i relacions entre dades que altres mètodes, com PCA, no poden reflectir. No obstant això, t-SNE també té limitacions importants: és computacionalment costós i no conserva bé les relacions globals entre punts, per la qual cosa pot distorsionar la imatge general de les dades.

    El fet que t-SNE només ofereixi el mètode fit_transform però no transform és una limitació important, especialment si es vol aplicar el model de t-SNE a noves dades. Això significa que cada vegada que es vol transformar un conjunt de dades, s'ha de recalcular tota la transformació des de zero, cosa que és molt costosa en termes de temps i recursos computacionals.

    Una alternativa que aborda algunes de les limitacions de t-SNE és UMAP (Uniform Manifold Approximation and Projection). UMAP ofereix avantatges similars a t-SNE pel que fa a la capacitat de capturar relacions locals i visualitzar agrupacions en espais de baixa dimensió, però amb alguns beneficis addicionals. UMAP és generalment més ràpid i escalable, i té tant un mètode fit com un mètode transform, la qual cosa el fa molt més eficient per aplicar a noves dades sense haver de recalcular tota la transformació. A més, UMAP preserva millor les relacions globals entre punts, cosa que permet visualitzacions que conserven tant la informació local com global.

    Conjunts desequilibrats de dades (2.5 punts)¶

    En els problemes de classificació, és molt comú trobar conjunts de dades molt desequilibrats. En la indústria existeixen múltiples exemples, com ara la detecció de frau o la fuga de clients. Aquest apartat se centra en l’anàlisi d’aquest tipus de conjunts.

    El cas del dataset amb el qual estem treballant (Bank Marketing) és un d’ells, ja que podem observar com la classe "no" apareix amb una freqüència fins a deu vegades més gran que la classe "yes".

    A continuació, analitzarem la distribució del nostre conjunt de dades. Per fer-ho, farem servir la funció show_distribution definida a la següent cel·la:

    In [27]:
    def show_distribution(y_df):
        freq = y_df["y"].value_counts()
        plt.pie(freq, labels=('No subscription ('+str(freq["no"])+')', 'Subscription ('+str(freq["yes"])+')'), autopct='%1.1f%%')
        plt.title("Term deposit subscription distribution")
        plt.show()
    
    In [28]:
    show_distribution(y)
    
    No description has been provided for this image

    Com es pot observar, el conjunt està força desequilibrat, ja que, pràcticament, només una desena part de les mostres corresponen a la contractació del dipòsit.

    Per tractar el problema de dades desequilibrades, analitzarem la tècnica de sobremostreig (oversampling) de la classe minoritària. A la literatura hi ha més tècniques per afrontar aquest problema, com el submostreig (undersampling) de la classe majoritària, però en aquesta PAC, ens centrarem només en la tècnica de sobremostreig.

    Oversampling¶

    Exercici: incrementeu les mostres de la classe minoritària fins a arribar a un nombre similar al dels elements de la classe majoritària aplicant les tècniques següents:
    • Duplicació aleatòria (_random over-sampling_), fixant random_state=10.
    • SMOTE (_Synthetic Minority Over-sampling Technique_), fixant random_state=10.
    • ADASYN (_Adaptive Synthetic Sampling_), fixant random_state=10.

    Per acabar, verifiqueu amb l’ajuda de la funció show_distribution, que després de l’aplicació d’aquestes tècniques, el nombre de mostres de la classe minoritària s’ha igualat al de la majoritària.


    Suggeriment: per aplicar la replicació aleatòria podeu fer servir "RandomOverSampler" d'imblearn.
    Suggeriment: per aplicar SMOTE podeu utilitzar "SMOTE" d'imblearn.
    Suggeriment: per aplicar ADASYN podeu utilitzar "ADASYN" d'imblearn.
    In [29]:
    from imblearn.over_sampling import RandomOverSampler
    
    numbers_df = df[numeric_columns]
    numbers_df
    
    # Creant un objecte RandomOverSampler
    ros = RandomOverSampler(random_state=10)
    
    # Aplicant Random Over-Sampling
    X_resampled_ros, y_resampled_ros = ros.fit_resample(numbers_df, y)
    
    show_distribution(y_resampled_ros)
    
    No description has been provided for this image
    In [30]:
    y_resampled_ros.describe()
    
    Out[30]:
    y
    count 79844
    unique 2
    top no
    freq 39922
    In [31]:
    from imblearn.over_sampling import SMOTE
    sm = SMOTE(random_state=10)
    X_res_smote, y_res_smote = sm.fit_resample(numbers_df, y)
    
    
    show_distribution(y_res_smote)
    
    No description has been provided for this image
    In [32]:
    y_res_smote.describe()
    
    Out[32]:
    y
    count 79844
    unique 2
    top no
    freq 39922
    In [33]:
    from imblearn.over_sampling import ADASYN
    
    ada = ADASYN(random_state=10)
    X_res_adasyn, y_res_adasyn = ada.fit_resample(numbers_df, y)
    
    show_distribution(y_res_adasyn)
    
    No description has been provided for this image
    In [34]:
    y_res_adasyn.head()
    
    Out[34]:
    y
    0 no
    1 no
    2 no
    3 no
    4 no

    El resultat d'aplicar aquestes tècniques hauria d'haver produit un nombre similar de mostres per a les dues classes. No obstant això, cadascun dels mètodes genera les noves mostres de la classe minoritària de manera diferent. Amb l'objectiu de comprendre millor i de manera visual com es generen aquestes noves mostres, a partir d'ara, farem servir la descomposició a dues dimensions que hagi mostrat un millor comportament a l'apartat anterior.

    Exercici: mostreu, amb un gràfic de dispersió en funció de les dues components resultants de la reducció dimensional prèvia, la distribució de les contractacions del conjunt de dades original, i del que s'ha obtingut després d'aplicar Random Over Sampling.
    In [35]:
    from sklearn.preprocessing import StandardScaler
    
    scaler = StandardScaler()
    norm_data_oversampled = scaler.fit_transform(X_resampled_ros)
    normdf_oversampled_ros = pd.DataFrame(norm_data_oversampled,columns=numdf.columns)
    normdf_oversampled_ros.head(10)
    
    Out[35]:
    age balance day_of_week duration campaign pdays previous
    0 1.404682 0.187404 -1.253967 -0.333576 -0.55888 -0.489594 -0.341155
    1 0.231035 -0.472799 -1.253967 -0.647103 -0.55888 -0.489594 -0.341155
    2 -0.691117 -0.481231 -1.253967 -0.860871 -0.55888 -0.489594 -0.341155
    3 0.482531 -0.011531 -1.253967 -0.815267 -0.55888 -0.489594 -0.341155
    4 -0.691117 -0.481544 -1.253967 -0.513141 -0.55888 -0.489594 -0.341155
    5 -0.523453 -0.409715 -1.253967 -0.681306 -0.55888 -0.489594 -0.341155
    6 -1.110277 -0.342258 -1.253967 -0.458987 -0.55888 -0.489594 -0.341155
    7 0.063371 -0.481231 -1.253967 0.005602 -0.55888 -0.489594 -0.341155
    8 1.404682 -0.444068 -1.253967 -0.934977 -0.55888 -0.489594 -0.341155
    9 0.147203 -0.296662 -1.253967 -0.920726 -0.55888 -0.489594 -0.341155
    In [36]:
    from sklearn.decomposition import PCA
    
    pca = PCA(n_components=2)
    X_pca = pca.fit_transform(normdf_oversampled_ros)
    
    pca_df_ros = pd.DataFrame(X_pca, columns=['PC1', 'PC2'])
    
    # afegm "y" per posar els colors
    pca_df_ros['Target'] = y_resampled_ros
    
    plt.figure(figsize=(10, 8))
    sns.scatterplot(data=pca_df_ros, x='PC1', y='PC2', hue='Target', palette='coolwarm', alpha=0.7)
    plt.title('PCA (2 Components) amb Target (yes/no) i oversampling')
    plt.xlabel('Component Principal 1')
    plt.ylabel('Component Principal 2')
    plt.grid(True)
    plt.show()
    
    No description has been provided for this image
    In [37]:
    from sklearn.manifold import TSNE
    
    
    # convertim 'y' a binari
    df_TSNE = pd.DataFrame(y_resampled_ros, columns=['y'])
    df_TSNE['y_binary'] = df_TSNE['y'].apply(lambda x: 1 if x == 'yes' else 0)
    
    tsne = TSNE(n_components=2, learning_rate=100, perplexity=30, random_state=10, early_exaggeration=12)
    X_tsne = tsne.fit_transform(normdf_oversampled_ros)  
    
    tsne_df = pd.DataFrame(X_tsne, columns=['Component 1', 'Component 2'])
    tsne_df['Target'] = df_TSNE['y_binary']
    
    plt.figure(figsize=(10, 8))
    sns.scatterplot(data=tsne_df, x='Component 1', y='Component 2', hue='Target', palette='coolwarm', alpha=0.7)
    plt.title('t-SNE amb Target (1/0) i oversampling')
    plt.xlabel('Component 1')
    plt.ylabel('Component 2')
    plt.grid(True)
    plt.show()
    
    No description has been provided for this image
    Anàlisi: Quines diferències i similituds trobes en les dues imatges anteriors? Justifiqueu la resposta tenint en compte la distribució d’ambdós conjunts, és a dir, la quantitat d’avaries de motor.

    Ara, les classes yes i no tenen una representació més equitativa, el que fa que les dues classes tinguin una presència visual similar en l'espai reduït a 2 dimensions. Això permet que les tècniques de visualització mostrin una separació més clara i una distribució més homogènia de les classes, la qual cosa no era possible abans, amb la distribució desbalancejada. Gràcies a equiparar les dades en la columna de la variable objectiu, podem obtenir uns gràfics que ens permeten diferenciar les dues classes amb més facilitat, doncs en els dos casos podem observar que les opcions de "yes" i "no", es mantenen en zones que podriem arribar a estimar la seva posició.

    Exercici: mostreu, amb un gràfic de dispersió en funció de les dues components resultants de la reducció dimensional prèvia, la distribució de les contractacions dels tres conjunts de dades obtinguts en aplicar les tres tècniques de sobremostreig.
    In [38]:
    scaler = StandardScaler()
    norm_data_oversampled_smote = scaler.fit_transform(X_res_smote)
    normdf_oversampled_smote = pd.DataFrame(norm_data_oversampled_smote,columns=numdf.columns)
    
    norm_data_oversampled_adasyn = scaler.fit_transform(X_res_adasyn)
    normdf_oversampled_adasyn = pd.DataFrame(norm_data_oversampled_adasyn,columns=numdf.columns)
    
    def pca_dataframe(X, y):
        pca = PCA(n_components=2)
        X_pca = pca.fit_transform(X)
        pca_df = pd.DataFrame(X_pca, columns=['PC1', 'PC2'])
        pca_df['Target'] = y
        return pca_df
    
    def tsne_dataframe(X, y):
        tsne = TSNE(n_components=2, learning_rate=100, perplexity=30, random_state=10, early_exaggeration=12)
        X_tsne = tsne.fit_transform(X)
        tsne_df = pd.DataFrame(X_tsne, columns=['Dim1', 'Dim2'])
        tsne_df['Target'] = y
        return tsne_df
    
    pca_df_ros = pca_dataframe(normdf_oversampled_ros, y_resampled_ros)
    pca_df_smote = pca_dataframe(normdf_oversampled_smote, y_res_smote)
    pca_df_adasyn = pca_dataframe(normdf_oversampled_adasyn, y_res_adasyn)
    
    tsne_df_ros = tsne_dataframe(normdf_oversampled_ros, y_resampled_ros)
    tsne_df_smote = tsne_dataframe(normdf_oversampled_smote, y_res_smote)
    tsne_df_adasyn = tsne_dataframe(normdf_oversampled_adasyn, y_res_adasyn)
    
    fig, axs = plt.subplots(2, 3, figsize=(18, 12), sharex='row', sharey='row')
    
    sns.scatterplot(data=pca_df_ros, x='PC1', y='PC2', hue='Target', palette='coolwarm', alpha=0.7, ax=axs[0, 0])
    axs[0, 0].set_title('RandomOverSampler PCA')
    axs[0, 0].set_xlabel('PC1')
    axs[0, 0].set_ylabel('PC2')
    
    sns.scatterplot(data=pca_df_smote, x='PC1', y='PC2', hue='Target', palette='coolwarm', alpha=0.7, ax=axs[0, 1])
    axs[0, 1].set_title('SMOTE PCA')
    axs[0, 1].set_xlabel('PC1')
    axs[0, 1].set_ylabel('')
    
    sns.scatterplot(data=pca_df_adasyn, x='PC1', y='PC2', hue='Target', palette='coolwarm', alpha=0.7, ax=axs[0, 2])
    axs[0, 2].set_title('ADASYN PCA')
    axs[0, 2].set_xlabel('PC1')
    axs[0, 2].set_ylabel('')
    
    sns.scatterplot(data=tsne_df_ros, x='Dim1', y='Dim2', hue='Target', palette='coolwarm', alpha=0.7, ax=axs[1, 0])
    axs[1, 0].set_title('RandomOverSampler t-SNE')
    axs[1, 0].set_xlabel('Dim1')
    axs[1, 0].set_ylabel('Dim2')
    
    sns.scatterplot(data=tsne_df_smote, x='Dim1', y='Dim2', hue='Target', palette='coolwarm', alpha=0.7, ax=axs[1, 1])
    axs[1, 1].set_title('SMOTE t-SNE')
    axs[1, 1].set_xlabel('Dim1')
    axs[1, 1].set_ylabel('')
    
    sns.scatterplot(data=tsne_df_adasyn, x='Dim1', y='Dim2', hue='Target', palette='coolwarm', alpha=0.7, ax=axs[1, 2])
    axs[1, 2].set_title('ADASYN t-SNE')
    axs[1, 2].set_xlabel('Dim1')
    axs[1, 2].set_ylabel('')
    
    plt.suptitle('Distribució PCA i t-SNE per a cada tècnica d\'oversampling')
    plt.tight_layout()
    plt.show()
    
    No description has been provided for this image
    Anàlisi: considerant el nombre de mostres amb contractació en cada conjunt, comenteu les diferències i semblances a les imatges de l’exercici anterior. Justifiqueu la resposta tenint en compte el comportament de cadascuna de les tècniques utilitzades.

    Random Over-Sampling simplement duplica mostres existents de manera aleatòria. No afegeix noves dades sintètiques, de manera que la distribució de punts no varia respecte a l'original; només augmenta la seva quantitat per igualar la classe majoritària.

    SMOTE genera noves mostres fent una interpolació entre els veïns més propers, fet que provoca una distribució més fluida i contínua de les dades. Aquesta tècnica és efectiva per evitar el sobreajustament i afegir variabilitat sintètica.

    ADASYN ajusta la quantitat de mostres generades segons la distribució de les dades, amb un enfocament adaptatiu que afavoreix les àrees més crítiques. Això permet que la classe minoritària es distribueixi millor en les àrees on es troben menys mostres, equilibrant més eficientment les classes.

    Tant SMOTE com ADASYN creen mostres sintètiques, però mentre SMOTE genera mostres entre els punts existents de manera més homogènia, ADASYN se centra més en les àrees menys denses, creant una distribució més equilibrada i adaptativa.